/*  QOpenGLWidget.cpp:290 多线程渲染官方说明
    在 Qt 中进行离屏渲染的一些注意事项和要求。离屏渲染是指在一个单独的线程中进
    行渲染操作，然后将渲染结果传递给 GUI/main 线程进行显示。

    Qt 中支持在工作线程中进行离屏渲染，比如生成纹理等，可以通过暴露
    QOpenGLContext 来实现， 在每个线程上创建共享的上下文。

    如果想要在工作线程中直接绘制到 QOpenGLWidget 的帧缓冲区而不影响 GUI/main
    线程，可以通过重写 paintEvent() 方法使其不做任何绘制，然后通过
    QObject::moveToThread() 更改上下文的线程关联。在这之后， 可以在工作线程上
    使用 makeCurrent() 和 doneCurrent() 方法。但是需要小心，在完成渲染后将上
    下文移回 GUI/main 线程。

    与QGLWidget不同，无法直接触发QOpenGLWidget的帧缓冲区进行缓冲区交换，因为
    QOpenGLWidget 没有真正的屏幕原生表面。相反，由窗口部件栈来管理合成和缓冲区
    交换，应当在 GUI/main 线程上调用 update() 来安排合成操作。

    在进行离屏渲染时，需要特别小心避免在 GUI/main 线程进行合成时使用帧缓冲区。
    在合成开始和结束时，aboutToCompose() 和 frameSwapped() 信号会被发出，
    它们会在GUI/main线程上被触发。这意味着如果使用直接连接，aboutToCompose()
    会阻塞 GUI/main 线程，直到工作线程完成渲染。之后，工作线程必须在
    frameSwapped() 信号被发出之前不再进行渲染。如果无法满足这个要求，工作线程必
    须实现双缓冲机制，这涉及使用另外的渲染目标，完全由工作线程控制，例如使用额外
    的帧缓冲对象，并在合适的时间将其复制到 QOpenGLWidget 的帧缓冲区。

    以上是关于 Qt 中离屏渲染的简要说明。离屏渲染通常用于在不阻塞主线程的情况下
    执行耗时的 渲染操作，提高应用程序的响应性能和用户体验。
 */

#include "qrender/render_view.h"

#include <cmath>

// OpenGL 相关
#include <QOpenGLShaderProgram>
#include <QOffscreenSurface>
#include <QOpenGLFramebufferObject>
#include <QOpenGLExtraFunctions>

#include <QTimer>
#include <QMutexLocker>
#include <QWaitCondition>
#include <QDebug>
#include <QTime>
#include <QTimerEvent>
#include <QWheelEvent>
#include <QApplication>
#include <QAbstractScrollArea>

#include <qrender/render_manager.h>

RenderView::RenderView(QWidget *parent) : QOpenGLWidget(parent)
{
    QSurfaceFormat format;
    format.setRenderableType(QSurfaceFormat::OpenGL);
    format.setDepthBufferSize(24);
    format.setStencilBufferSize(8);
    format.setVersion(3, 3);
    format.setProfile(QSurfaceFormat::CoreProfile);
    setFormat(format);

    // 3. 清除缓冲区/保留模式不清除
    // 在每次渲染新帧之前，需要清除颜色缓冲区、深度缓冲区和模板缓冲区，以准备绘制新的图像。
    // QOpenGLWidget 内部实现
    setUpdateBehavior(QOpenGLWidget::PartialUpdate); // 启用保留模式

    // 判断小控件是否映射到屏幕上
    // 要在构造或重新创建窗口中设为 true 一次, 否则第一次显示时不生效,
    // 最小化再显示后才可以
    setAttribute(Qt::WA_Mapped, true);

    // 启用按键事件拦截
    setFocusPolicy(Qt::StrongFocus);

    // 处理特殊事件
    installEventFilter(this);

    // 构造多线程渲染管理器
    render_manager_.reset(new RenderManager(this));
    connect(render_manager_.get(), &RenderManager::blitFramebuffer, this,
            &RenderView::onBlitFramebuffer);

    update_timer_id_ = startTimer(50);
}

RenderView::~RenderView()
{
    // 释放资源并将上下文所有权移回此线程。
    render_manager_->mtx_render_.lock();
    render_manager_->requestStop();
    render_manager_->cond_render_.wait(&render_manager_->mtx_render_);
    render_manager_->mtx_render_.unlock();
}

bool RenderView::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == this) {
        if (event->type() == QEvent::ParentAboutToChange) {
            // 判断小控件是否映射到屏幕上
            // 要在构造或重新创建窗口中设为 true 一次, 否则第一次显示时不生效,
            // 最小化再显示后才可以
            setAttribute(Qt::WA_Mapped, true);
        }

        if (event->type() == QEvent::LayoutRequest) {
            // FIXME: 切换页面会有残影
            // 在 demo 中并无此情况
            // 下列方案可解决, 并且比信号槽通知的方式更简洁
            QTimer::singleShot(0, this, [=] { refresh(qApp->activeWindow()); });
        }
    }

    return QOpenGLWidget::eventFilter(watched, event);
}

void RenderView::addRenderer(std::shared_ptr<Renderer> renderer)
{
    render_manager_->addRenderer(renderer);
}

bool RenderView::paintEnabled() const
{
    return paint_enabled_;
}

void RenderView::setPaintEnabled(bool paint_enabled)
{
    paint_enabled_ = paint_enabled;
}

QPixmap RenderView::grab(const QRect &rectangle)
{
    // 确保可显示界面
    while (!update_enabled_) {
        QApplication::processEvents(QEventLoop::AllEvents, 100);
    }

    grabbing_ = true;
    QPixmap p = QOpenGLWidget::grab(rectangle);
    grabbing_ = false;

    return p;
}

void RenderView::initializeGL()
{
    // 1. 准备场景数据
    // 在合成之前，需要准备场景所需的数据，包括顶点坐标、纹理坐标、颜色、光照等信息。
    // 这些数据将用于描述场景中的对象和图形。

    // 创建过, 先释放再创建
    if (render_manager_->ctx_) {
        // 释放资源并将上下文所有权移回此线程。
        render_manager_->mtx_render_.lock();
        render_manager_->requestStop();
        render_manager_->cond_render_.wait(&render_manager_->mtx_render_);
        render_manager_->mtx_render_.unlock();
    }

    // 初始化 OpenGL 上下文
    surface_ = new QOffscreenSurface(context()->screen(), this);
    surface_->setFormat(context()->format());
    surface_->create();

    if (!context()->makeCurrent(surface_)) {
        qWarning("QOpenGLContext::makeCurrent failed.");
        return;
    }

    QOpenGLFunctions *f = context()->functions();
    f->initializeOpenGLFunctions();

    // 打印当前 OpenGL 版本
    GLint major, minor;
    f->glGetIntegerv(GL_MAJOR_VERSION, &major); // gl大版本（整型）
    f->glGetIntegerv(GL_MINOR_VERSION, &minor); // gl小版本（整型）

    qDebug() << "OpenGL Version Info:"
             << (const char *)f->glGetString(GL_VERSION);
    qDebug() << "OpenGL Version Major:" << major
             << "OpenGL Version Minor:" << minor;

    // 解决切换页面默认为黑色问题
    f->glClearColor(1.0, 1.0, 1.0, 1.0);

    // 初始化多线程渲染管理器
    render_manager_->requestInit();

    // 2. 设置窗口和投影
    // 在渲染之前，需要设置视口（Viewport）和投影矩阵，以定义绘制的区域和透视效果。
    // 2.1 QOpenGLWidget 实现视口矩阵
}

void RenderView::resizeGL(int w, int h)
{
    qDebug() << __PRETTY_FUNCTION__ << w << ":" << h;
    render_manager_->requestResize();
}

void RenderView::paintGL()
{
    // 2.2 Camera 及子线程内部实现 投影矩阵

    // 4. 绘制场景
    // 通过OpenGL绘制指令，将场景中的对象和图形渲染到帧缓冲区。
    // 这里不需要进行实际的渲染，渲染任务在渲染线程中执行
    // 等待上次渲染并拷贝帧缓冲区完成

    // 5. 合成和渲染
    // OpenGL将渲染的图形合成到帧缓冲区中，按照混合和透明度等设置来控制像素的合成方式。
    // 双缓冲模式，子线程合成和渲染在帧缓冲区中后，再拷贝到 QOpenGLWidget 中
    if (grabbing_) {
        render_manager_->mtx_render_.lock();
        blockSignals(true);
    }

    if (update_enabled_) {
        update_enabled_ = false;
        render_manager_->requestRender();
    }

    if (grabbing_) {
        render_manager_->cond_render_.wait(&render_manager_->mtx_render_);
        render_manager_->mtx_render_.unlock();
        blockSignals(false);
        onBlitFramebuffer();
    }
}

void RenderView::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == update_timer_id_) {
        // 定时刷新，后台不渲染
        if (updatesEnabled() && testAttribute(Qt::WA_Mapped) &&
            paint_enabled_) {
            update();
        }
    }

    return QOpenGLWidget::timerEvent(event);
}

void RenderView::paintEvent(QPaintEvent *event)
{
    /* 方案1
     * 如果想要在工作线程中直接绘制到 QOpenGLWidget 的帧缓冲区而不影响 GUI/main
     * 线程，可以通过重写 paintEvent() 方法使其不做任何绘制，然后通过
     * QObject::moveToThread() 更改上下文的线程关联。在这之后， 可以在工作线程
     * 上使用 makeCurrent() 和 doneCurrent() 方法。但是需要小心，在完成渲染后
     * 将上下文移回 GUI/main 线程。
     */
    // 拦截下列 QOpenGLWidget::paintEvent 内容，
    // Q_UNUSED(e);
    // Q_D(QOpenGLWidget);
    // if (!d->initialized)
    //     return;

    // if (updatesEnabled())
    //     d->render();

    // 方案2：
    // 在子线程中使用帧缓冲区，在绘制时将其复制到 QOpenGLWidget 的帧缓冲区中。
    // 在 onBlitFramebuffer 实现，此处不再拦截事件。
    return QOpenGLWidget::paintEvent(event);
}

void RenderView::setUpdateInterval(int interval)
{
    killTimer(update_timer_id_);
    update_timer_id_ = startTimer(interval);
}

void RenderView::refresh(QWidget *wd)
{
    if (!wd) {
        return;
    }

    wd->update();
    foreach (auto scrollArea, wd->findChildren<QAbstractScrollArea *>()) {
        if (!scrollArea) {
            continue;
        }
        scrollArea->viewport()->update();
        foreach (auto wd_sca_child, scrollArea->findChildren<QWidget *>()) {
            if (!wd_sca_child) {
                continue;
            }
            refresh(wd_sca_child);
        }
    }
}

void RenderView::onBlitFramebuffer()
{
    // 调用 update 渲染到这里有延迟, 期间可能不用显示了但才触发槽函数
    if (!grabbing_) {
        if (!updatesEnabled() || !testAttribute(Qt::WA_Mapped) ||
            !paint_enabled_) {
            update_enabled_ = true;
            return;
        }
    }

    QMutexLocker lk(&render_manager_->mtx_render_);

    makeCurrent();

    // 切换目标时(如将本 widget 放到不同父窗口下), 会重置上下文
    if (!context() || !render_manager_->fbo_) {
        update_enabled_ = true;
        return;
    }

    // 拷贝帧缓冲区 fbo
    auto f = context()->extraFunctions();
    f->glBindFramebuffer(GL_READ_FRAMEBUFFER, render_manager_->fbo_->handle());
    f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, defaultFramebufferObject());
    f->glBlitFramebuffer(0, 0, render_manager_->fbo_->size().width(),
                         render_manager_->fbo_->size().height(), 0, 0, width(),
                         height(), GL_COLOR_BUFFER_BIT, GL_NEAREST);
    f->glBindFramebuffer(GL_FRAMEBUFFER, 0);

    doneCurrent();

    // 允许下一帧绘制
    update_enabled_ = true;

    // 6. 帧缓冲区交换
    // 当一帧的渲染和合成完成后，帧缓冲区中的图像需要交换到屏幕上进行显示。
    // 这个步骤称为帧缓冲区交换，也称为双缓冲机制。
    // 在双缓冲中，有两个帧缓冲区，一个用于渲染和合成，另一个用于显示。
    // 渲染完成后，它们进行交换，以避免在显示过程中看到不完整或不稳定的图像。
    // 随后，渲染缓冲区将准备好接收下一帧的渲染，循环进行渲染和交换，从而实现连续的动画效果。
    // QOpenGLWidget 内部实现
}
